#include <stdio.h>

#include "mtd.h"
#include "nand.h"


#define ALLOCATE(x) malloc(x)
#define FREE(x)     free(x)

#define STATIC 
#define ENOMEM 12

int NAND_SHIFT = 11;
int PAGE_DATA_SIZE = 2048;
int PAGE_SPARE_SIZE = 64;
int BLK_SHIFT = 6;
int PAGES_PER_BLOCK = 64;

int PAGE_TOTAL_SIZE;
int BLOCK_TOTAL_SIZE;
//#define BLOCKS_PER_MEG ((1<<20)/(PAGES_PER_BLOCK * PAGE_DATA_SIZE))

typedef	unsigned int uint32_t;
typedef	unsigned int u_int32_t;
typedef unsigned int size_t;

typedef unsigned char __u8;
typedef unsigned short __u16;
typedef unsigned int __u32;

typedef struct 
{
	__u8 *data; // Data + spare
	int empty;      // is this empty?
} nand_driver_Page;

struct kvec {
	void *iov_base; 
	size_t iov_len;
};

typedef struct
{
	nand_driver_Page *page[256];
	int damaged;	
} nand_driver_Block;

typedef struct
{
	nand_driver_Block**block;
	int nBlocks;
} nand_driver_Device;

extern unsigned int totalBlocks,totalBytesPerBlock,totalBytesPerChunk,chunkSize,spareSize,chunksPerBlock,dataBytesPerBlock;
extern unsigned int totalSize;

STATIC nand_driver_Device ndd;
STATIC int sizeInMB = 2048;
//STATIC struct mtd_info nand_mtd;

struct nand_ecclayout nand_oob_16 = {
	.eccbytes = 6,
	.eccpos = {0, 1, 2, 3, 6, 7},
	.oobfree = {
		{.offset = 8,
		 .length = 8}}
};

struct nand_ecclayout nand_oob_64 = {
	.eccbytes = 24,
	.eccpos = {
		   40, 41, 42, 43, 44, 45, 46, 47,
		   48, 49, 50, 51, 52, 53, 54, 55,
		   56, 57, 58, 59, 60, 61, 62, 63},
	.oobfree = {
		{.offset = 2,//FIXME zjyu88
		 .length = 38}}
};

STATIC unsigned char scan_ff_pattern[] = { 0xff, 0xff };

STATIC void nand_driver_yield(int n)
{
}

int nand_driver_GetBytesPerChunk(void) 
{ 
    return PAGE_DATA_SIZE;
}

int nand_driver_GetChunksPerBlock(void) 
{ 
    return PAGES_PER_BLOCK; 
}

int nand_driver_GetNumberOfBlocks(void) 
{
    return nand_driver_CalcNBlocks();
}

STATIC void nand_driver_Read(void *buffer, int page, int start, int nBytes)
{
	int pg = page%PAGES_PER_BLOCK;
	int blk = page/PAGES_PER_BLOCK;
	if(buffer && nBytes > 0)
	{
		memcpy(buffer,&ndd.block[blk]->page[pg]->data[start],nBytes);
	}
	
}

STATIC void nand_driver_Program(const void *buffer, int page, int start, int nBytes)
{
	int pg = page%PAGES_PER_BLOCK;
	int blk = page/PAGES_PER_BLOCK;
	__u8 *p;
	__u8 *b = (__u8 *)buffer;

	p = &ndd.block[blk]->page[pg]->data[start];
	
	while(buffer && nBytes>0)
	{
		*p = *b;
		p++;
		b++;
		nBytes--;
	}
}

STATIC void nand_driver_DoErase(int blockNumber)
{
	int i;
	
	nand_driver_Block *blk;
	
	if(blockNumber < 0 || blockNumber >= ndd.nBlocks)
	{
		return;
	}
	
	blk = ndd.block[blockNumber];
	
	for(i = 0; i < PAGES_PER_BLOCK; i++)
	{
		memset(blk->page[i]->data,0xff,PAGE_DATA_SIZE);
		blk->page[i]->empty = 1;
	}
	nand_driver_yield(2);
}

STATIC int nand_driver_CalcNBlocks(void)
{
	return totalBlocks;
}

STATIC int nandAllocMem = 0;	

STATIC int  CheckInit(void)
{
	int i,j;	
	int fail = 0;
	int nBlocks; 
	int nAllocated = 0;
	
	if(nandAllocMem) 
	{
		return 0;
	}	
	
	ndd.nBlocks = nBlocks = nand_driver_CalcNBlocks();	
	ndd.block = ALLOCATE(sizeof(nand_driver_Block*) * nBlocks );
	
	if(!ndd.block) 
		return ENOMEM;		
		
	for(i=fail=0; i <nBlocks && !fail; i++)
	{		
		nand_driver_Block *blk;
		
		if(!(blk = ndd.block[i] = ALLOCATE(sizeof(nand_driver_Block))))
		{
		    fail = 1;
		}  
		else
		{
			for(j = 0; j < PAGES_PER_BLOCK; j++)
			{
				if((blk->page[j] = ALLOCATE(sizeof(nand_driver_Page))) != 0)
				{
					if((blk->page[j]->data = ALLOCATE(PAGE_TOTAL_SIZE)) == 0)//FIXME zjyu88
						fail = 1;
				}
				else
					fail = 1;
			}
			nand_driver_DoErase(i);
			ndd.block[i]->damaged = 0;
			nAllocated++;
		}
	}
	
	if(fail)
	{	    
		//Todo thump pages		
		for(; i >=0; i--)
		{
		    nand_driver_Block *blk;

			blk = ndd.block[i];			
		       for(j = 0; j < PAGES_PER_BLOCK; j++)
	       		{
			    if(blk->page[j])
			    {
				FREE(blk->page[j]->data);
				FREE(blk->page[j]);
			    }
	       		}
			FREE(ndd.block[i]);
		}
		FREE(ndd.block);
		ndd.block = 0;
		return ENOMEM;
	}
	
	ndd.nBlocks = nBlocks;	
	nandAllocMem = 1;
	
	return 1;
}

STATIC void nand_driver_CleanUp(void)
{
	int i,j;
	
	for(i = 0; i < ndd.nBlocks; i++)
	{
		for(j = 0; j < PAGES_PER_BLOCK; j++)
		{
		   FREE(ndd.block[i]->page[j]->data);
		   FREE(ndd.block[i]->page[j]);
		}
		FREE(ndd.block[i]);
		
	}
	FREE(ndd.block);
	ndd.block = 0;
}



STATIC int nand_driver_ReadId(__u8 *vendorId, __u8 *deviceId)
{
	*vendorId = 'Y'; 
	*deviceId = '2';
	
	return 1;
}

STATIC int nand_driver_ReadStatus(__u8 *status)
{
		*status = 0;
		return 1;
}

#ifdef CONFIG_MTD_NAND_ECC
#include <linux/mtd/nand_ecc.h>
#endif

/*
 * NAND low-level MTD interface functions
 */
STATIC int nand_read (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf);
STATIC int nand_read_oob (struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops);
STATIC int nand_write (struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf);
STATIC int nand_write_oob (struct mtd_info *mtd, loff_t to, size_t len,
				size_t *retlen, const u_char *buf);
STATIC int nand_writev (struct mtd_info *mtd, const struct kvec *vecs,
				unsigned long count, loff_t to, size_t *retlen);
STATIC int nand_erase (struct mtd_info *mtd, struct erase_info *instr);
STATIC void nand_sync (struct mtd_info *mtd);


/*
 * NAND read
 */
STATIC int nand_read (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf)
{
	return nand_read_ecc (mtd, from, len, retlen, buf, NULL,NULL);
}

/*
 * NAND read with ECC
 */
STATIC int nand_read_ecc (struct mtd_info *mtd, loff_t from, size_t len,
				size_t *retlen, u_char *buf, u_char *oob_buf,struct nand_oobinfo *oobsel)
{
	int 	start, page;
	int   leavingSize = len;
	int nToCopy;

	/* Initialize return value */
	*retlen = 0;

	/* Do not allow reads past end of device */
	if ((from + len) > totalSize) {
		return -EINVAL;
	}

	if((start + leavingSize) < chunkSize)
	{
		nToCopy = len;
	}
	else
	{
		nToCopy =  chunkSize - start;
	}
		
	while(leavingSize > 0)
	{
		page = from / chunkSize;
		start = from & (chunkSize - 1);

		nand_driver_Read(buf, page, start, nToCopy);
		if(oob_buf)
		    nand_driver_Read(oob_buf,page,PAGE_DATA_SIZE,PAGE_SPARE_SIZE);

		leavingSize -= nToCopy;
		from += nToCopy;
		buf += nToCopy;
		if(oob_buf) 
			oob_buf += PAGE_SPARE_SIZE;

		if(leavingSize>chunkSize)
			nToCopy = leavingSize-chunkSize;
		else
			nToCopy = leavingSize;
	}
	
	*retlen = len-leavingSize;

	return 0;
}


STATIC int nand_do_read_oob(struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops)
{
	int readlen = ops->ooblen;
	unsigned int readSize = 0,readTmp=0;
	int len;	
	uint8_t *buf = NULL;

       buf = ops->oobbuf;
	if (ops->mode == MTD_OOB_AUTO)
		len = nand_oob_64.oobfree[0].length;
	else
		len = spareSize;

	if (ops->ooboffs >= len) {
		DEBUG (0, "nand_do_read_oob: Attempt to start read outside oob\n");
		return -EINVAL;
	}

	/* Do not allow reads past end of device */
	if (from >= mtd->size){
		DEBUG (0, "%s(%s:%d): Reading size(%d) is greater than mtd size(%d).\n",__FUNCTION__,__FILE__,__LINE__,from,mtd->size);
		return -EINVAL;
	}
		
        if(ops->ooboffs + readlen > (totalBlocks -(from /totalBytesPerBlock)) * len) {
		DEBUG (0, "%s(%s:%d): Attempt read beyond end of device\n",__FUNCTION__,__FILE__,__LINE__);
		return -EINVAL;
	}

	while(1) {
		readSize = min(len, readlen);
		readTmp = nand_read_oob_low(mtd, buf, (unsigned int)from,ops->ooboffs,ops->mode, readSize);

		readlen -= readTmp;
		buf += readTmp;
		from += chunkSize;
		if (!readlen)
			break;
	}

	ops->oobretlen = ops->ooblen;
	return 0;
}


STATIC int nand_do_read_ops(struct mtd_info *mtd, loff_t from,struct mtd_oob_ops *ops)
{
	__u8 *oob, *buf;
	uint32_t col=0,bytes=0,ooblen=0,len=0;
	uint32_t readlen = ops->len;
	uint32_t oobreadlen = ops->ooblen;

	col = (int)from % chunkSize;
        
	buf = ops->datbuf;
	oob = ops->oobbuf;

	if(ops->mode == MTD_OOB_RAW )
	{
	    if(readlen % chunkSize)
    	    {
    	        DEBUG(0,"nand_do_read_ops:readlen is not aligned to %d\n",chunkSize);
		 readlen += chunkSize - readlen % chunkSize;
    	    }
	    if(oobreadlen % spareSize)
    	    {
    	        DEBUG(0,"nand_do_read_ops:oobreadlen is not aligned to %d\n",spareSize);
		 oobreadlen += spareSize - readlen % spareSize;
    	    }	
	    if(from % totalBytesPerChunk)
    	    {
    	        DEBUG(0,"nand_do_read_ops:start address is not aligned to %d\n",totalBytesPerChunk);
		 from -=  from % totalBytesPerChunk;
    	    }			
	}

	if (ops->mode == MTD_OOB_AUTO)
		len = nand_oob_64.oobfree[0].length;
	else
		len = spareSize;

	while(1) 
	{
		bytes = min(chunkSize - col, readlen);
		ooblen = min(len , oobreadlen);
		
		bytes = nand_read_data_low(mtd, buf, (unsigned int)from, (size_t)bytes);
		ooblen = nand_read_oob_low(mtd, oob, (unsigned int)from,ops->ooboffs, ops->mode, ooblen);
		
		readlen -= bytes;
		oobreadlen -= ooblen;
		buf += bytes;
		oob += ooblen;

		from += chunkSize;

		if(readlen <=0 && oobreadlen <=0 )
			break;
	}  

	return 0;
}


 STATIC int nand_read_oob(struct mtd_info *mtd, loff_t from,
			 struct mtd_oob_ops *ops)
{
	int ret = 0;

	ops->retlen = 0;

	switch(ops->mode) {
	case MTD_OOB_PLACE:
	case MTD_OOB_AUTO:
	case MTD_OOB_RAW:
		break;

	default:
		DEBUG(0,"ops mode %d is error\n",ops->mode);
		return -1;
	}

	if (!ops->datbuf)
		ret = nand_do_read_oob(mtd, from, ops);
	else
		ret = nand_do_read_ops(mtd, from, ops);

	return ret;
}

STATIC int nand_read_data_low (struct mtd_info *mtd, uint8_t *buf, unsigned int from, size_t len)
{
	unsigned int col, page;
	unsigned int retLen = 0;

       if( len <= 0 )
	   	return 0;
	   
	/* Shift to get page */
	page = ((unsigned int) from) / chunkSize;
	/* Mask to get column */
	col = from % chunkSize ;
       
       if(col+len > chunkSize )
   	{
	    DEBUG(0,"%s(%s:%d)read size(start position:%d,size:%d) is greater than %d\n",__FUNCTION__,__FILE__,__LINE__,from,len,chunkSize);
	    return 0;
   	}

	retLen = len;
	nand_driver_Read(buf,page,col,retLen);

	/* Return happy */
	return retLen;
}

STATIC int nand_read_oob_low (struct mtd_info *mtd, uint8_t *buf, unsigned int from,unsigned int ooboff,mtd_oob_mode_t mode, unsigned int len)
{
	unsigned int col, page;
	unsigned int retLen = 0;

       if(len <= 0 )
	   	return 0;
	/* Shift to get page */
	page = ((int) from) / chunkSize;
	/* Mask to get column */
	col = ooboff & (spareSize-1);

	switch(mode) {	
       case MTD_OOB_AUTO:		
	   	col += nand_oob_64.oobfree[0].offset;
		break;
		
	default:
		break;
	}
       
       if(col+len > nand_oob_64.oobfree[0].length )
   	{
	    DEBUG(0,"nand_read_oob_low:read size is greater than %d\n",nand_oob_64.oobfree[0].length);
   	}

	if( nand_oob_64.oobfree[0].length-col < len)
		retLen = nand_oob_64.oobfree[0].length-col;
	else
		retLen = len;
	
	nand_driver_Read(buf,page,PAGE_DATA_SIZE + col,retLen);

	/* Return happy */
	return retLen;
}

/*
 * NAND write
 */
STATIC int nand_write (struct mtd_info *mtd, loff_t to, size_t len,size_t *retlen, const u_char *buf)
{
	return nand_write_ecc (mtd, to, len, retlen, buf, NULL,NULL);
}

/*
 * NAND write with ECC
 */
STATIC int nand_write_ecc (struct mtd_info *mtd, loff_t to, size_t len,
				size_t *retlen, const u_char *buf,
				u_char *oob_buf, struct nand_oobinfo *dummy)
{
	int start, page;
	int n = len;
	int nToCopy;

	/* Do not allow reads past end of device */
	if ((to + len) > mtd->size) {
		*retlen = 0;
		return -EINVAL;
	}

	/* Initialize return value */
	*retlen = 0;

	while(n > 0)
	{
		/* First we calculate the starting page */
		page = to >> NAND_SHIFT;
		/* Get raw starting column */
		start = to & (PAGE_DATA_SIZE - 1);

		// OK now check for the curveball where the start and end are in
		// the same page
		if((start + n) < PAGE_DATA_SIZE)
		{
			nToCopy = n;
		}
		else
		{
			nToCopy =  PAGE_DATA_SIZE - start;
		}

		nand_driver_Program(buf, page, start, nToCopy);
		nand_driver_Program(oob_buf, page, PAGE_DATA_SIZE, PAGE_SPARE_SIZE);

		n -= nToCopy;
		to += nToCopy;
		buf += nToCopy;
		if(oob_buf) 
			oob_buf += PAGE_SPARE_SIZE;
		*retlen += nToCopy;
	}

	return 0;
}

/*
 * NAND write out-of-band
 */
STATIC int nand_write_oob (struct mtd_info *mtd, loff_t to, size_t len,
				size_t *retlen, const u_char *buf)
{
	int col, page;


	T(0,(
		"nand_read_oob: to = 0x%08x, len = %i\n", (unsigned int) to,
		(int) len));

	/* Shift to get page */
	page = ((int) to) >> NAND_SHIFT;

	/* Mask to get column */
	col = to & PAGE_SPARE_SIZE;

	/* Initialize return length value */
	*retlen = 0;

	/* Do not allow reads past end of device */
	if ((to + len) > mtd->size) {
		T(0,(
		   "nand_read_oob: Attempt read beyond end of device\n"));
		*retlen = 0;
		return -EINVAL;
	}

	nand_driver_Program(buf,page,512 + col,len);

	/* Return happy */
	*retlen = len;
	return 0;

}

/*
 * NAND write with iovec
 */
STATIC int nand_writev (struct mtd_info *mtd, const struct kvec *vecs,unsigned long count, loff_t to, size_t *retlen)
{
	return -EINVAL;
}

/*
 * NAND erase a block
 */
STATIC int nand_erase (struct mtd_info *mtd, struct erase_info *instr)
{
	int i, nBlocks,block;

	T(0,(
		"nand_erase: start = 0x%08x, len = %i\n",
		(unsigned int) instr->addr, (unsigned int) instr->len));

	/* Start address must align on block boundary */
	if (instr->addr & (mtd->erasesize - 1)) {
		T(0,(
			"nand_erase: Unaligned address\n"));
		return -EINVAL;
	}

	/* Length must align on block boundary */
	if (instr->len & (mtd->erasesize - 1)) {
		T(0,(
			"nand_erase: Length not block aligned\n"));
		return -EINVAL;
	}

	/* Do not allow erase past end of device */
	if ((instr->len + instr->addr) > mtd->size) {
		T(0,(
			"nand_erase: Erase past end of device\n"));
		return -EINVAL;
	}

	nBlocks = instr->len >> (NAND_SHIFT + BLK_SHIFT);
	block = instr->addr >> (NAND_SHIFT + BLK_SHIFT);

	for(i = 0; i < nBlocks; i++)
	{
		nand_driver_DoErase(block);
		block++;
	}
	
	instr->state = MTD_ERASE_DONE; /* Changed state to done */
	instr->callback(instr);	       /* ... and wake up */

	return 0;


}


STATIC int nand_block_isbad(struct mtd_info *mtd, loff_t ofs)
{
    struct mtd_oob_ops ops;
    unsigned char oobbuf[64];
	
    ops.mode = MTD_OOB_RAW;
    ops.datbuf = NULL;
    ops.ooboffs = 0;
    ops.ooblen = 2;
    ops.oobbuf = oobbuf;

    return 0;
/*
    nand_read_oob(mtd,ofs,&ops);
    if( (oobbuf[0] != scan_ff_pattern[0]) || (oobbuf[1] != scan_ff_pattern[1]))
    {
       printf("(%s:%d)oobuf[0]:0x%02x,oobuf[1]:0x%02x,scan_ff_pattern[0]:0x%02x,scan_ff_pattern[1]:0x%02x\n",__FUNCTION__,__LINE__,oobbuf[0],oobbuf[1],
	   	scan_ff_pattern[0],scan_ff_pattern[1]);	
	return -1;
    }

    return 0;
*/
}

STATIC int nand_block_markbad(struct mtd_info *mtd, loff_t ofs)
{
	return 0;
}


/*
 * NAND sync
 */
STATIC void nand_sync (struct mtd_info *mtd)
{
	T(0,("nand_sync: called\n"));
}

/*
 * Scan for the NAND device
 */
STATIC void nand_scan (struct mtd_info *mtd,int nchips)
{
	mtd->writesize = PAGE_DATA_SIZE;
	mtd->oobsize   = PAGE_SPARE_SIZE;
	mtd->oobavail  = PAGE_SPARE_SIZE/2; /* Simulate using up some for other uses */
	mtd->erasesize = PAGE_DATA_SIZE * PAGES_PER_BLOCK;
	mtd->size = totalSize;//sizeInMB * 1024*1024;
	sizeInMB = totalSize >> 20;
	nand_oob_64.oobfree[0].length = PAGE_SPARE_SIZE;//FIXME zjyu88

	/* Fill in remaining MTD driver data */
	mtd->type = 4;
	mtd->flags = 0x400;
	mtd->owner = 0;
	mtd->erase = nand_erase;
	mtd->point = NULL;
	mtd->unpoint = NULL;
	mtd->read = nand_read;
	mtd->write = nand_write;
	mtd->read_oob = nand_read_oob;
	mtd->write_oob = nand_write_oob;
	mtd->block_isbad = nand_block_isbad;
	mtd->block_markbad = nand_block_markbad;
/*	mtd->readv = NULL;*/
	mtd->writev = nand_writev;
	mtd->sync = nand_sync;
	mtd->lock = NULL;
	mtd->unlock = NULL;
	mtd->suspend = NULL;
	mtd->resume = NULL;

	mtd->name = "NAND";
}


/*
 * Main initialization routine
 */
int nand_init (struct mtd_info *mtd)
{
	// Do the nand init FIXME zjyu88
	PAGE_DATA_SIZE = chunkSize;
	PAGE_SPARE_SIZE = spareSize;
	PAGES_PER_BLOCK = chunksPerBlock;

	PAGE_TOTAL_SIZE = PAGE_DATA_SIZE+PAGE_SPARE_SIZE;
	BLOCK_TOTAL_SIZE = PAGES_PER_BLOCK * PAGE_TOTAL_SIZE;
	if(PAGE_DATA_SIZE > 2048)
	{
		NAND_SHIFT = 12;
		BLK_SHIFT = 7;
	}
	else
	{
		NAND_SHIFT = 11;
		BLK_SHIFT = 6;	
	}

	/* allocate memory */
	CheckInit();
	nand_scan(mtd,1);
	return 0;
}

/* obsolated: nand_mtd is no longer a global variable. */
//void * nand_get_ptr(void)
//{
//    return (void *)&nand_mtd;
//}

int nand_read_img(int nFd, int totalSize)
{
    unsigned int readSizePerTimes = totalBytesPerChunk;
    unsigned int readingBlock = 0,readingChunk=0;
    unsigned int readingSize = 0;

    int nBlocks = 0;
	//printf("in read image\n");
    nBlocks = nand_driver_CalcNBlocks();
    if( !ndd.block )
		return 0;

    for(;readingBlock < nBlocks;readingBlock++)
    {
		nand_driver_Block *blk;

		blk = ndd.block[readingBlock];

		for(readingChunk = 0; readingChunk < PAGES_PER_BLOCK; readingChunk++)
		{
		    int nReadBytes = 0;
		    int nLen = 0;
		    int nTryTimes = 10000;	
			
		    nand_driver_Page *page;	
			
		    page = blk->page[readingChunk];
		    #if 0
		    read(nFd,page->data,PAGE_TOTAL_SIZE);
		    #else
		    while( (nReadBytes<PAGE_TOTAL_SIZE) && nTryTimes--)
		    {
		        nLen = read(nFd,page->data+nReadBytes,PAGE_TOTAL_SIZE-nReadBytes);
			 nReadBytes += nLen;
			 readingSize += nLen;
		    }
		    if((nReadBytes<PAGE_TOTAL_SIZE) && !nTryTimes)
		    {
		        DEBUG(0,"%s(%s:%d):tried times too much\n",__FUNCTION__,__FILE__,__LINE__);
		        return 0;
		    }
		    #endif
		}
        
    }

    return totalSize;
}
